/*
This file is part of MMM.

MMM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

MMM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with MMM.  If not, see <http://www.gnu.org/licenses/>.
*
* @package    MMM
* @author     Andre Meixner
* @copyright  2018 High Performance Humanoid Technologies (H2T), Karlsruhe, Germany
*
*/

#ifndef __MMM_ADDATTACHEDSENSORCONFIGURATION_H_
#define __MMM_ADDATTACHEDSENSORCONFIGURATION_H_

#include <vector>
#include <string>
#include <regex>
#include <boost/algorithm/string.hpp>
#include <QSettings>

#include <MMM/Motion/Sensor.h>
#include <MMM/Motion/AttachedSensor.h>
#include <MMM/Motion/Plugin/IMUPlugin/IMUSensor.h>

namespace MMM
{

class BaseAddAttachedSensorConfiguration
{
public:
    virtual IAttachedSensorPtr loadSensorConfiguration(MMM::RapidXMLReaderNodePtr configuration) = 0;
    virtual IAttachedSensorPtr getAttachedSensor() = 0;
    virtual SensorPtr getSensor() = 0;
    virtual void setName(const std::string &name) = 0;
    virtual void setDescription(const std::string &description) = 0;
    virtual void setSegment(const std::string &segment) = 0;
    virtual void setOffsetUnit(const std::string &offsetUnit) = 0;
    virtual void setOffset(const Eigen::Matrix4f &offset) = 0;
    virtual void setConfiguration(const std::string &segment, const Eigen::Matrix4f &offset, const std::string &offsetUnit, const std::string &description = std::string(), const std::string &name = std::string()) = 0;
    virtual std::string getPreview(const std::string &sensorFile, bool ignoreFirstLine, const std::string &firstSplitChar, bool byRegex, const std::string &secondSplitChar, const std::string &regex, const std::vector<int> &indexes, const std::string &timestamp = std::string()) = 0;
    virtual std::string addSensorMeasurements(const std::string &sensorFile, bool ignoreFirstLine, const std::string &firstSplitChar, bool byRegex, const std::string &secondSplitChar, const std::string &regex, const std::vector<int> &indexes, const std::string &timestamp = std::string()) = 0;
    virtual std::vector<std::string> getIndexNames() = 0;
    virtual void loadConfiguration(bool &ignoreFirstLine, std::string &firstSplitChar, bool &byRegex, std::string &secondSplitChar, std::string &regex, std::vector<int> &indexes) = 0;
    virtual std::string getName() = 0;
    virtual std::string getSensorName() = 0;
};

typedef boost::shared_ptr<BaseAddAttachedSensorConfiguration> BaseAddAttachedSensorConfigurationPtr;

// S : IAttachedSensor, Sensor
template <typename S, typename std::enable_if<std::is_base_of<IAttachedSensor, S>::value>::type* = nullptr, typename std::enable_if<std::is_base_of<Sensor, S>::value>::type* = nullptr>
class AddAttachedSensorConfiguration : public BaseAddAttachedSensorConfiguration
{
public:
    AddAttachedSensorConfiguration(const std::string &sensorName, const std::vector<std::string> &indexNames) :
        sensor(new S()),
        sensorName(sensorName),
        name(sensorName + "converter")
    {
        this->indexNames.push_back(TIMESTEP_STR);
        for (const auto &name : indexNames) this->indexNames.push_back(name);
    }

    IAttachedSensorPtr loadSensorConfiguration(MMM::RapidXMLReaderNodePtr configuration) {
        IAttachedSensor::loadSensorConfiguration(sensor, configuration);
        return sensor;
    }

    IAttachedSensorPtr getAttachedSensor() {
        return sensor;
    }

    SensorPtr getSensor() {
        return sensor;
    }

    void setName(const std::string &name) {
        sensor->setUniqueName(name);
    }

    void setDescription(const std::string &description) {
        sensor->setDescription(description);
    }

    void setSegment(const std::string &segment) {
        sensor->setSegment(segment);
    }

    void setOffsetUnit(const std::string &offsetUnit) {
        sensor->setOffsetUnit(offsetUnit);
    }

    void setOffset(const Eigen::Matrix4f &offset) {
        sensor->setOffset(offset);
    }

    void setConfiguration(const std::string &segment, const Eigen::Matrix4f &offset, const std::string &offsetUnit, const std::string &description = std::string(), const std::string &name = std::string()) {
        setSegment(segment);
        setOffsetUnit(offsetUnit);
        setOffset(offset);
        setDescription(description);
        setName(name);
    }

    std::string getPreview(const std::string &sensorFile, bool ignoreFirstLine, const std::string &firstSplitChar, bool byRegex, const std::string &secondSplitChar, const std::string &regex, const std::vector<int> &indexes, const std::string &timestamp = std::string()) {
        std::vector<std::map<std::string, std::string> > values = getValues(sensorFile, ignoreFirstLine, firstSplitChar, byRegex, secondSplitChar, regex, indexes, timestamp);
        return createPreviewString(values);
    }

    std::string addSensorMeasurements(const std::string &sensorFile, bool ignoreFirstLine, const std::string &firstSplitChar, bool byRegex, const std::string &secondSplitChar, const std::string &regex, const std::vector<int> &indexes, const std::string &timestamp = std::string()) {
        std::vector<std::map<std::string, std::string> > values = getValues(sensorFile, ignoreFirstLine, firstSplitChar, byRegex, secondSplitChar, regex, indexes, timestamp);
        addSensorMeasurements(values);
        saveConfiguration(ignoreFirstLine, firstSplitChar, byRegex, secondSplitChar, regex, indexes);
        RapidXMLWriterPtr writer(new RapidXMLWriter());
        RapidXMLWriterNodePtr rootNode = writer->createRootNode("Preview");
        sensor->appendSensorXML(rootNode);
        return writer->print(true);
    }

    std::vector<std::string> getIndexNames() {
        return indexNames;
    }

    void loadConfiguration(bool &ignoreFirstLine, std::string &firstSplitChar, bool &byRegex, std::string &secondSplitChar, std::string &regex, std::vector<int> &indexes) {
        QSettings settings;
        ignoreFirstLine = settings.value(QString::fromStdString(name + "/standard/ignoreFirstLine"), 0).toBool();
        firstSplitChar = settings.value(QString::fromStdString(name + "/standard/firstSplitChar"), "\\n").toString().toStdString();
        byRegex = settings.value(QString::fromStdString(name + "/standard/byRegex"), 0).toBool();
        secondSplitChar = settings.value(QString::fromStdString(name + "/standard/secondSplitChar"), ";").toString().toStdString();
        regex = settings.value(QString::fromStdString(name + "/standard/regex"), "").toString().toStdString();
        for (unsigned int i = 0; i < indexNames.size(); i++) {
            indexes.push_back(settings.value(QString::fromStdString(name + "/standard/index/" + indexNames[i]), QVariant(i + 1)).toInt());
        }
    }

    std::string getName() {
        return name;
    }

    std::string getSensorName() {
        return sensorName;
    }

    static constexpr const char* TIMESTEP_STR = "timestep";

protected:
    virtual void addSensorMeasurements(const std::vector<std::map<std::string, std::string> > &values) = 0;

    boost::shared_ptr<S> sensor;

private:
    long long parseTime(const std::string &time) {
        std::regex re("(\\d+):(\\d+):(\\d+)(?:\\.(\\d+))?");
        std::smatch matches;
        std::regex_search(time, matches, re);
        std::vector<std::string> splittedTime;
        splittedTime.assign(matches.begin(), matches.end());
        long long timeInSeconds = MMM::XML::convertTo<int>(splittedTime.at(1)) * 60 * 60 + MMM::XML::convertTo<int>(splittedTime.at(2)) * 60 + MMM::XML::convertTo<int>(splittedTime.at(3));
        if (splittedTime.size() == 5) {
            double milliSeconds = MMM::XML::convertTo<double>("0." + splittedTime.at(4));
            timeInSeconds = timeInSeconds * 1000 + milliSeconds * 1000;
        }
        return timeInSeconds;
    }

    std::vector<std::map<std::string, std::string> > getValues(const std::string &sensorFile, bool ignoreFirstLine, const std::string &firstSplitChar, bool byRegex, const std::string &secondSplitChar, const std::string &regex, const std::vector<int> &indexes, const std::string &timestamp = std::string()) {
        std::vector<std::map<std::string, std::string> > values;
        std::string regexStr = regex;
        std::regex re;
        try {
            re = std::regex(regexStr);
        } catch (std::regex_error &e) {
            throw MMM::Exception::MMMException(e.what());
        }

        std::vector<std::string> sensorFileLines;
        if (firstSplitChar == "\\n") boost::split(sensorFileLines, sensorFile, boost::is_any_of("\n")); // hack
        else boost::split(sensorFileLines, sensorFile, boost::is_any_of(firstSplitChar));
        int lineIndex = 0;
        for (const std::string &sensorFileLine : sensorFileLines) {
            if (sensorFileLine.empty()) continue;
            if (lineIndex == 0 && ignoreFirstLine) {
                MMM_INFO << "Ignoring first line '" << sensorFileLine << "'." << std::endl;
                lineIndex++;
                continue;
            }
            std::vector<std::string> splittedStr;
            if (byRegex) {
                std::smatch matches;
                std::regex_search(sensorFileLine, matches, re);
                splittedStr.assign(matches.begin(), matches.end());
            }
            else boost::split(splittedStr, sensorFileLine,  boost::is_any_of(secondSplitChar));

            int maxCapturingGroupIndex = *std::max_element(indexes.begin(), indexes.end());
            if ((int) splittedStr.size() > maxCapturingGroupIndex) {
                std::map<std::string, std::string> value;
                for (unsigned int i = 0; i < indexNames.size(); i++) {
                    value[indexNames[i]] = splittedStr[indexes[i]];
                }
                values.push_back(value);
            }
            else {
                std::string error;
                if (byRegex) error = "Error in line " + std::to_string(lineIndex) + ": " + sensorFileLine + ".\n Not the appropriate amount of capturing groups. Having " + std::to_string(splittedStr.size()) + " capturing groups, but wanting at least " + std::to_string(maxCapturingGroupIndex + 1) + "!";
                else error = "Error in line " + std::to_string(lineIndex) + ": " + sensorFileLine + ".\n Not the appropriate amount of strings. Having only " + std::to_string(splittedStr.size()) + " strings, but wanting at least " + std::to_string(maxCapturingGroupIndex + 1) + "!";
                throw MMM::Exception::MMMException(error);
            }
            lineIndex++;
        }

        if (!timestamp.empty()) {
            // TODO use timestamp specifier
            long long first = -1.0;
            for (std::map<std::string, std::string> &m : values) {
                long long milliseconds = parseTime(m[AddAttachedSensorConfiguration::TIMESTEP_STR]);
                double seconds = MMM::Math::roundf(milliseconds / 1000.0);
                if (first == -1.0) {
                    first = milliseconds;
                    seconds = 0.0;
                }
                else seconds = MMM::Math::roundf((milliseconds - first) / 1000.0);
                m[AddAttachedSensorConfiguration::TIMESTEP_STR] = MMM::XML::toString(seconds);
            }
        }

        return values;
    }

    std::string createPreviewString(const std::vector<std::map<std::string, std::string> > &values) {
        std::string previewString;
        for (auto value : values) {
            for (const std::string &name : indexNames) {
                previewString += name + " = " + value[name] + "\n";
            }
            previewString += "\n";
        }
        return previewString;
    }

    void saveConfiguration(bool ignoreFirstLine, const std::string &firstSplitChar, bool byRegex, const std::string &secondSplitChar, const std::string &regex, const std::vector<int> &indexes) {
        if (indexes.size() != indexNames.size()) return;
        QSettings settings;
        settings.setValue(QString::fromStdString(name + "/standard/ignoreFirstLine"), ignoreFirstLine);
        settings.setValue(QString::fromStdString(name + "/standard/firstSplitChar"), QString::fromStdString(firstSplitChar));
        settings.setValue(QString::fromStdString(name + "/standard/byRegex"), byRegex);
        settings.setValue(QString::fromStdString(name + "/standard/secondSplitChar"), QString::fromStdString(secondSplitChar));
        settings.setValue(QString::fromStdString(name + "/standard/regex"), QString::fromStdString(regex));
        for (unsigned int i = 0; i < indexNames.size(); i++) {
            settings.setValue(QString::fromStdString(name + "/standard/index/" + indexNames[i]), QVariant(indexes[i]));
        }
    }

    std::string sensorName;
    std::string name;
    std::vector<std::string> indexNames;
};

}

#endif // __MMM_ADDATTACHEDSENSORCONFIGURATION_H_
